/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.search;

import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.WildcardQuery;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.function.FunctionQuery;
import org.apache.solr.search.function.QueryValueSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Collection of static utilities useful for query parsing.
 * 
 * @version $Id: QueryParsing.java 1152657 2011-07-31 22:43:13Z hossman $
 */
public class QueryParsing {
	public static final String OP = "q.op"; // the SolrParam used to override
											// the QueryParser
											// "default operator"
	public static final String V = "v"; // value of this parameter
	public static final String F = "f"; // field that a query or command
										// pertains to
	public static final String TYPE = "type";// type of this query or command
	public static final String DEFTYPE = "defType"; // default type for any
													// direct subqueries
	public static final String LOCALPARAM_START = "{!";
	public static final char LOCALPARAM_END = '}';
	public static final String DOCID = "_docid_";
	public static final String SCORE = "score";

	// true if the value was specified by the "v" param (i.e. v=myval, or
	// v=$param)
	public static final String VAL_EXPLICIT = "__VAL_EXPLICIT__";

	/**
	 * Helper utility for parsing a query using the Lucene QueryParser syntax.
	 * 
	 * @param qs
	 *            query expression in standard Lucene syntax
	 * @param schema
	 *            used for default operator (overridden by params) and passed to
	 *            the query parser for field format analysis information
	 */
	public static Query parseQuery(String qs, IndexSchema schema) {
		return parseQuery(qs, null, schema);
	}

	/**
	 * Helper utility for parsing a query using the Lucene QueryParser syntax.
	 * 
	 * @param qs
	 *            query expression in standard Lucene syntax
	 * @param defaultField
	 *            default field used for unqualified search terms in the query
	 *            expression
	 * @param schema
	 *            used for default operator (overridden by params) and passed to
	 *            the query parser for field format analysis information
	 */
	public static Query parseQuery(String qs, String defaultField,
			IndexSchema schema) {
		try {
			Query query = schema.getSolrQueryParser(defaultField).parse(qs);

			if (SolrCore.log.isTraceEnabled()) {
				SolrCore.log.trace("After QueryParser:" + query);
			}

			return query;

		} catch (ParseException e) {
			SolrCore.log(e);
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Error parsing Lucene query", e);
		}
	}

	/**
	 * Helper utility for parsing a query using the Lucene QueryParser syntax.
	 * 
	 * @param qs
	 *            query expression in standard Lucene syntax
	 * @param defaultField
	 *            default field used for unqualified search terms in the query
	 *            expression
	 * @param params
	 *            used to determine the default operator, overriding the schema
	 *            specified operator
	 * @param schema
	 *            used for default operator (overridden by params) and passed to
	 *            the query parser for field format analysis information
	 */
	public static Query parseQuery(String qs, String defaultField,
			SolrParams params, IndexSchema schema) {
		try {
			SolrQueryParser parser = schema.getSolrQueryParser(defaultField);
			String opParam = params.get(OP);
			if (opParam != null) {
				parser.setDefaultOperator("AND".equals(opParam) ? QueryParser.Operator.AND
						: QueryParser.Operator.OR);
			}
			Query query = parser.parse(qs);

			if (SolrCore.log.isTraceEnabled()) {
				SolrCore.log.trace("After QueryParser:" + query);
			}

			return query;

		} catch (ParseException e) {
			SolrCore.log(e);
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Query parsing error: " + e.getMessage(), e);
		}
	}

	// note to self: something needs to detect infinite recursion when parsing
	// queries
	static int parseLocalParams(String txt, int start,
			Map<String, String> target, SolrParams params)
			throws ParseException {
		int off = start;
		if (!txt.startsWith(LOCALPARAM_START, off))
			return start;
		StrParser p = new StrParser(txt, start, txt.length());
		p.pos += 2; // skip over "{!"

		for (;;) {
			/*
			 * if (p.pos>=txt.length()) { throw new
			 * ParseException("Missing '}' parsing local params '" + txt + '"');
			 * }
			 */
			char ch = p.peek();
			if (ch == LOCALPARAM_END) {
				return p.pos + 1;
			}

			String id = p.getId();
			if (id.length() == 0) {
				throw new ParseException(
						"Expected identifier '}' parsing local params '" + txt
								+ '"');

			}
			String val = null;

			ch = p.peek();
			if (ch != '=') {
				// single word... treat {!func} as type=func for easy lookup
				val = id;
				id = TYPE;
			} else {
				// saw equals, so read value
				p.pos++;
				ch = p.peek();
				boolean deref = false;
				if (ch == '$') {
					p.pos++;
					ch = p.peek();
					deref = true; // dereference whatever value is read by
									// treating it as a variable name
				}

				if (ch == '\"' || ch == '\'') {
					val = p.getQuotedString();
				} else {
					// read unquoted literal ended by whitespace or '}'
					// there is no escaping.
					int valStart = p.pos;
					for (;;) {
						if (p.pos >= p.end) {
							throw new ParseException(
									"Missing end to unquoted value starting at "
											+ valStart + " str='" + txt + "'");
						}
						char c = p.val.charAt(p.pos);
						if (c == LOCALPARAM_END || Character.isWhitespace(c)) {
							val = p.val.substring(valStart, p.pos);
							break;
						}
						p.pos++;
					}
				}

				if (deref) { // dereference parameter
					if (params != null) {
						val = params.get(val);
					}
				}
			}
			if (target != null)
				target.put(id, val);
		}
	}

	public static String encodeLocalParamVal(String val) {
		int len = val.length();
		int i = 0;
		if (len > 0 && val.charAt(0) != '$') {
			for (; i < len; i++) {
				char ch = val.charAt(i);
				if (Character.isWhitespace(ch) || ch == '}')
					break;
			}
		}

		if (i >= len)
			return val;

		// We need to enclose in quotes... but now we need to escape
		StringBuilder sb = new StringBuilder(val.length() + 4);
		sb.append('\'');
		for (i = 0; i < len; i++) {
			char ch = val.charAt(i);
			if (ch == '\'') {
				sb.append('\\');
			}
			sb.append(ch);
		}
		sb.append('\'');
		return sb.toString();
	}

	/**
	 * "foo" returns null "{!prefix f=myfield}yes" returns
	 * type="prefix",f="myfield",v="yes" "{!prefix f=myfield v=$p}" returns
	 * type="prefix",f="myfield",v=params.get("p")
	 */
	public static SolrParams getLocalParams(String txt, SolrParams params)
			throws ParseException {
		if (txt == null || !txt.startsWith(LOCALPARAM_START)) {
			return null;
		}
		Map<String, String> localParams = new HashMap<String, String>();
		int start = QueryParsing.parseLocalParams(txt, 0, localParams, params);

		String val = localParams.get(V);
		if (val == null) {
			val = txt.substring(start);
			localParams.put(V, val);
		} else {
			// localParams.put(VAL_EXPLICIT, "true");
		}
		return new MapSolrParams(localParams);
	}

	/**
	 * Returns null if the sortSpec is the standard sort desc.
	 * <p/>
	 * <p>
	 * The form of the sort specification string currently parsed is:
	 * </p>
	 * 
	 * <pre>>
	 * SortSpec ::= SingleSort [, SingleSort]*
	 * SingleSort ::= <fieldname> SortDirection
	 * SortDirection ::= top | desc | bottom | asc
	 * </pre>
	 * 
	 * Examples:
	 * 
	 * <pre>
	 *   score desc               #normal sort by score (will return null)
	 *   weight bottom            #sort by weight ascending
	 *   weight desc              #sort by weight descending
	 *   height desc,weight desc  #sort by height descending, and use weight descending to break any ties
	 *   height desc,weight asc   #sort by height descending, using weight ascending as a tiebreaker
	 * </pre>
	 */
	public static Sort parseSort(String sortSpec, SolrQueryRequest req) {
		if (sortSpec == null || sortSpec.length() == 0)
			return null;
		List<SortField> lst = new ArrayList<SortField>(4);

		try {

			StrParser sp = new StrParser(sortSpec);
			while (sp.pos < sp.end) {
				sp.eatws();

				final int start = sp.pos;

				// short circuit test for a really simple field name
				String field = sp.getId(null);
				Exception qParserException = null;

				if (field == null || !Character.isWhitespace(sp.peekChar())) {
					// let's try it as a function instead
					field = null;
					String funcStr = sp.val.substring(start);

					QParser parser = QParser.getParser(funcStr,
							FunctionQParserPlugin.NAME, req);
					Query q = null;
					try {
						if (parser instanceof FunctionQParser) {
							FunctionQParser fparser = (FunctionQParser) parser;
							fparser.setParseMultipleSources(false);
							fparser.setParseToEnd(false);

							q = fparser.getQuery();

							if (fparser.localParams != null) {
								if (fparser.valFollowedParams) {
									// need to find the end of the function
									// query via the string parser
									int leftOver = fparser.sp.end
											- fparser.sp.pos;
									sp.pos = sp.end - leftOver; // reset our
																// parser to the
																// same amount
																// of leftover
								} else {
									// the value was via the "v" param in
									// localParams, so we need to find
									// the end of the local params themselves to
									// pick up where we left off
									sp.pos = start + fparser.localParamsEnd;
								}
							} else {
								// need to find the end of the function query
								// via the string parser
								int leftOver = fparser.sp.end - fparser.sp.pos;
								sp.pos = sp.end - leftOver; // reset our parser
															// to the same
															// amount of
															// leftover
							}
						} else {
							// A QParser that's not for function queries.
							// It must have been specified via local params.
							q = parser.getQuery();

							assert parser.getLocalParams() != null;
							sp.pos = start + parser.localParamsEnd;
						}

						Boolean top = sp.getSortDirection();
						if (null != top) {
							// we have a Query and a valid direction
							if (q instanceof FunctionQuery) {
								lst.add(((FunctionQuery) q).getValueSource()
										.getSortField(top));
							} else {
								lst.add((new QueryValueSource(q, 0.0f))
										.getSortField(top));
							}
							continue;
						}
					} catch (IOException ioe) {
						throw ioe;
					} catch (Exception e) {
						// hang onto this in case the string isn't a full field
						// name either
						qParserException = e;
					}
				}

				// if we made it here, we either have a "simple" field name,
				// or there was a problem parsing the string as a complex
				// func/quer

				if (field == null) {
					// try again, simple rules for a field name with no
					// whitespace
					sp.pos = start;
					field = sp.getSimpleString();
				}
				Boolean top = sp.getSortDirection();
				if (null == top) {
					throw new SolrException(
							SolrException.ErrorCode.BAD_REQUEST,
							"Can't determine Sort Order: " + sp);
				}

				if (SCORE.equals(field)) {
					if (top) {
						lst.add(SortField.FIELD_SCORE);
					} else {
						lst.add(new SortField(null, SortField.SCORE, true));
					}
				} else if (DOCID.equals(field)) {
					lst.add(new SortField(null, SortField.DOC, top));
				} else {
					// try to find the field
					SchemaField sf = req.getSchema().getFieldOrNull(field);
					if (null == sf) {
						if (null != qParserException) {
							throw new SolrException(
									SolrException.ErrorCode.BAD_REQUEST,
									"sort param could not be parsed as a query, and is not a "
											+ "field that exists in the index: "
											+ field, qParserException);
						}
						throw new SolrException(
								SolrException.ErrorCode.BAD_REQUEST,
								"sort param field can't be found: " + field);
					}
					lst.add(sf.getSortField(top));
				}
			}

		} catch (ParseException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"error in sort: " + sortSpec, e);
		} catch (IOException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"error in sort: " + sortSpec, e);
		}

		// normalize a sort on score desc to null
		if (lst.size() == 1 && lst.get(0) == SortField.FIELD_SCORE) {
			return null;
		}

		return new Sort(lst.toArray(new SortField[lst.size()]));
	}

	// /////////////////////////
	// /////////////////////////
	// /////////////////////////

	static FieldType writeFieldName(String name, IndexSchema schema,
			Appendable out, int flags) throws IOException {
		FieldType ft = null;
		ft = schema.getFieldTypeNoEx(name);
		out.append(name);
		if (ft == null) {
			out.append("(UNKNOWN FIELD " + name + ')');
		}
		out.append(':');
		return ft;
	}

	static void writeFieldVal(String val, FieldType ft, Appendable out,
			int flags) throws IOException {
		if (ft != null) {
			try {
				out.append(ft.indexedToReadable(val));
			} catch (Exception e) {
				out.append("EXCEPTION(val=");
				out.append(val);
				out.append(")");
			}
		} else {
			out.append(val);
		}
	}

	/**
	 * @see #toString(Query,IndexSchema)
	 */
	public static void toString(Query query, IndexSchema schema,
			Appendable out, int flags) throws IOException {
		boolean writeBoost = true;

		if (query instanceof TermQuery) {
			TermQuery q = (TermQuery) query;
			Term t = q.getTerm();
			FieldType ft = writeFieldName(t.field(), schema, out, flags);
			writeFieldVal(t.text(), ft, out, flags);
		} else if (query instanceof TermRangeQuery) {
			TermRangeQuery q = (TermRangeQuery) query;
			String fname = q.getField();
			FieldType ft = writeFieldName(fname, schema, out, flags);
			out.append(q.includesLower() ? '[' : '{');
			String lt = q.getLowerTerm();
			String ut = q.getUpperTerm();
			if (lt == null) {
				out.append('*');
			} else {
				writeFieldVal(lt, ft, out, flags);
			}

			out.append(" TO ");

			if (ut == null) {
				out.append('*');
			} else {
				writeFieldVal(ut, ft, out, flags);
			}

			out.append(q.includesUpper() ? ']' : '}');
		} else if (query instanceof NumericRangeQuery) {
			NumericRangeQuery q = (NumericRangeQuery) query;
			String fname = q.getField();
			FieldType ft = writeFieldName(fname, schema, out, flags);
			out.append(q.includesMin() ? '[' : '{');
			Number lt = q.getMin();
			Number ut = q.getMax();
			if (lt == null) {
				out.append('*');
			} else {
				out.append(lt.toString());
			}

			out.append(" TO ");

			if (ut == null) {
				out.append('*');
			} else {
				out.append(ut.toString());
			}

			out.append(q.includesMax() ? ']' : '}');
		} else if (query instanceof BooleanQuery) {
			BooleanQuery q = (BooleanQuery) query;
			boolean needParens = false;

			if (q.getBoost() != 1.0 || q.getMinimumNumberShouldMatch() != 0) {
				needParens = true;
			}
			if (needParens) {
				out.append('(');
			}
			boolean first = true;
			for (BooleanClause c : q.clauses()) {
				if (!first) {
					out.append(' ');
				} else {
					first = false;
				}

				if (c.isProhibited()) {
					out.append('-');
				} else if (c.isRequired()) {
					out.append('+');
				}
				Query subQuery = c.getQuery();
				boolean wrapQuery = false;

				// TODO: may need to put parens around other types
				// of queries too, depending on future syntax.
				if (subQuery instanceof BooleanQuery) {
					wrapQuery = true;
				}

				if (wrapQuery) {
					out.append('(');
				}

				toString(subQuery, schema, out, flags);

				if (wrapQuery) {
					out.append(')');
				}
			}

			if (needParens) {
				out.append(')');
			}
			if (q.getMinimumNumberShouldMatch() > 0) {
				out.append('~');
				out.append(Integer.toString(q.getMinimumNumberShouldMatch()));
			}

		} else if (query instanceof PrefixQuery) {
			PrefixQuery q = (PrefixQuery) query;
			Term prefix = q.getPrefix();
			FieldType ft = writeFieldName(prefix.field(), schema, out, flags);
			out.append(prefix.text());
			out.append('*');
		} else if (query instanceof ConstantScorePrefixQuery) {
			ConstantScorePrefixQuery q = (ConstantScorePrefixQuery) query;
			Term prefix = q.getPrefix();
			FieldType ft = writeFieldName(prefix.field(), schema, out, flags);
			out.append(prefix.text());
			out.append('*');
		} else if (query instanceof WildcardQuery) {
			out.append(query.toString());
			writeBoost = false;
		} else if (query instanceof FuzzyQuery) {
			out.append(query.toString());
			writeBoost = false;
		} else if (query instanceof ConstantScoreQuery) {
			out.append(query.toString());
			writeBoost = false;
		} else {
			out.append(query.getClass().getSimpleName() + '('
					+ query.toString() + ')');
			writeBoost = false;
		}

		if (writeBoost && query.getBoost() != 1.0f) {
			out.append("^");
			out.append(Float.toString(query.getBoost()));
		}

	}

	/**
	 * Formats a Query for debugging, using the IndexSchema to make complex
	 * field types readable.
	 * <p/>
	 * <p>
	 * The benefit of using this method instead of calling
	 * <code>Query.toString</code> directly is that it knows about the data
	 * types of each field, so any field which is encoded in a particularly
	 * complex way is still readable. The downside is that it only knows about
	 * built in Query types, and will not be able to format custom Query
	 * classes.
	 * </p>
	 */
	public static String toString(Query query, IndexSchema schema) {
		try {
			StringBuilder sb = new StringBuilder();
			toString(query, schema, sb, 0);
			return sb.toString();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Simple class to help with parsing a string <b>Note: This API is
	 * experimental and may change in non backward-compatible ways in the
	 * future</b>
	 */
	public static class StrParser {
		String val;
		int pos;
		int end;

		public StrParser(String val) {
			this(val, 0, val.length());
		}

		public StrParser(String val, int start, int end) {
			this.val = val;
			this.pos = start;
			this.end = end;
		}

		void eatws() {
			while (pos < end && Character.isWhitespace(val.charAt(pos)))
				pos++;
		}

		char ch() {
			return pos < end ? val.charAt(pos) : 0;
		}

		void skip(int nChars) {
			pos = Math.max(pos + nChars, end);
		}

		boolean opt(String s) {
			eatws();
			int slen = s.length();
			if (val.regionMatches(pos, s, 0, slen)) {
				pos += slen;
				return true;
			}
			return false;
		}

		boolean opt(char ch) {
			eatws();
			if (val.charAt(pos) == ch) {
				pos++;
				return true;
			}
			return false;
		}

		void expect(String s) throws ParseException {
			eatws();
			int slen = s.length();
			if (val.regionMatches(pos, s, 0, slen)) {
				pos += slen;
			} else {
				throw new ParseException("Expected '" + s + "' at position "
						+ pos + " in '" + val + "'");
			}
		}

		float getFloat() throws ParseException {
			eatws();
			char[] arr = new char[end - pos];
			int i;
			for (i = 0; i < arr.length; i++) {
				char ch = val.charAt(pos);
				if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-'
						|| ch == '.' || ch == 'e' || ch == 'E') {
					pos++;
					arr[i] = ch;
				} else {
					break;
				}
			}

			return Float.parseFloat(new String(arr, 0, i));
		}

		Number getNumber() throws ParseException {
			eatws();
			int start = pos;
			boolean flt = false;

			while (pos < end) {
				char ch = val.charAt(pos);
				if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') {
					pos++;
				} else if (ch == '.' || ch == 'e' || ch == 'E') {
					flt = true;
					pos++;
				} else {
					break;
				}
			}

			String v = val.substring(start, pos);
			return flt ? Double.parseDouble(v) : Long.parseLong(v);
		}

		double getDouble() throws ParseException {
			eatws();
			char[] arr = new char[end - pos];
			int i;
			for (i = 0; i < arr.length; i++) {
				char ch = val.charAt(pos);
				if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-'
						|| ch == '.' || ch == 'e' || ch == 'E') {
					pos++;
					arr[i] = ch;
				} else {
					break;
				}
			}

			return Double.parseDouble(new String(arr, 0, i));
		}

		int getInt() throws ParseException {
			eatws();
			char[] arr = new char[end - pos];
			int i;
			for (i = 0; i < arr.length; i++) {
				char ch = val.charAt(pos);
				if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') {
					pos++;
					arr[i] = ch;
				} else {
					break;
				}
			}

			return Integer.parseInt(new String(arr, 0, i));
		}

		String getId() throws ParseException {
			return getId("Expected identifier");
		}

		String getId(String errMessage) throws ParseException {
			eatws();
			int id_start = pos;
			char ch;
			if (pos < end && (ch = val.charAt(pos)) != '$'
					&& Character.isJavaIdentifierStart(ch)) {
				pos++;
				while (pos < end) {
					ch = val.charAt(pos);
					if (!Character.isJavaIdentifierPart(ch) && ch != '.') {
						break;
					}
					pos++;
				}
				return val.substring(id_start, pos);
			}

			if (errMessage != null) {
				throw new ParseException(errMessage + " at pos " + pos
						+ " str='" + val + "'");
			}
			return null;
		}

		/**
		 * Skips leading whitespace and returns whatever sequence of non
		 * whitespace it can find (or hte empty string)
		 */
		String getSimpleString() {
			eatws();
			int startPos = pos;
			char ch;
			while (pos < end) {
				ch = val.charAt(pos);
				if (Character.isWhitespace(ch))
					break;
				pos++;
			}
			return val.substring(startPos, pos);
		}

		/**
		 * Sort direction or null if current position does not inidcate a sort
		 * direction. (True is desc, False is asc). Position is advanced to
		 * after the comma (or end) when result is non null
		 */
		Boolean getSortDirection() throws ParseException {
			final int startPos = pos;
			final String order = getId(null);

			Boolean top = null;

			if (null != order) {
				if ("desc".equals(order) || "top".equals(order)) {
					top = true;
				} else if ("asc".equals(order) || "bottom".equals(order)) {
					top = false;
				}

				// it's not a legal direction if more stuff comes after it
				eatws();
				final char c = ch();
				if (0 == c) {
					// :NOOP
				} else if (',' == c) {
					pos++;
				} else {
					top = null;
				}
			}

			if (null == top)
				pos = startPos; // no direction, reset
			return top;
		}

		// return null if not a string
		String getQuotedString() throws ParseException {
			eatws();
			char delim = peekChar();
			if (!(delim == '\"' || delim == '\'')) {
				return null;
			}
			int val_start = ++pos;
			StringBuilder sb = new StringBuilder(); // needed for escaping
			for (;;) {
				if (pos >= end) {
					throw new ParseException(
							"Missing end quote for string at pos "
									+ (val_start - 1) + " str='" + val + "'");
				}
				char ch = val.charAt(pos);
				if (ch == '\\') {
					pos++;
					if (pos >= end)
						break;
					ch = val.charAt(pos);
					switch (ch) {
					case 'n':
						ch = '\n';
						break;
					case 't':
						ch = '\t';
						break;
					case 'r':
						ch = '\r';
						break;
					case 'b':
						ch = '\b';
						break;
					case 'f':
						ch = '\f';
						break;
					case 'u':
						if (pos + 4 >= end) {
							throw new ParseException(
									"bad unicode escape \\uxxxx at pos"
											+ (val_start - 1) + " str='" + val
											+ "'");
						}
						ch = (char) Integer.parseInt(
								val.substring(pos + 1, pos + 5), 16);
						pos += 4;
						break;
					}
				} else if (ch == delim) {
					pos++; // skip over the quote
					break;
				}
				sb.append(ch);
				pos++;
			}

			return sb.toString();
		}

		// next non-whitespace char
		char peek() {
			eatws();
			return pos < end ? val.charAt(pos) : 0;
		}

		// next char
		char peekChar() {
			return pos < end ? val.charAt(pos) : 0;
		}

		@Override
		public String toString() {
			return "'" + val + "'" + ", pos=" + pos;
		}

	}

	/**
	 * Builds a list of String which are stringified versions of a list of
	 * Queries
	 */
	public static List<String> toString(List<Query> queries, IndexSchema schema) {
		List<String> out = new ArrayList<String>(queries.size());
		for (Query q : queries) {
			out.add(QueryParsing.toString(q, schema));
		}
		return out;
	}

	/**
	 * Parse a function, returning a FunctionQuery
	 * <p/>
	 * <p>
	 * Syntax Examples....
	 * </p>
	 * <p/>
	 * 
	 * <pre>
	 * // Numeric fields default to correct type
	 * // (ie: IntFieldSource or FloatFieldSource)
	 * // Others use explicit ord(...) to generate numeric field value
	 * myfield
	 * <p/>
	 * // OrdFieldSource
	 * ord(myfield)
	 * <p/>
	 * // ReverseOrdFieldSource
	 * rord(myfield)
	 * <p/>
	 * // LinearFloatFunction on numeric field value
	 * linear(myfield,1,2)
	 * <p/>
	 * // MaxFloatFunction of LinearFloatFunction on numeric field value or constant
	 * max(linear(myfield,1,2),100)
	 * <p/>
	 * // ReciprocalFloatFunction on numeric field value
	 * recip(myfield,1,2,3)
	 * <p/>
	 * // ReciprocalFloatFunction on ReverseOrdFieldSource
	 * recip(rord(myfield),1,2,3)
	 * <p/>
	 * // ReciprocalFloatFunction on LinearFloatFunction on ReverseOrdFieldSource
	 * recip(linear(rord(myfield),1,2),3,4,5)
	 * </pre>
	 */
	public static FunctionQuery parseFunction(String func, IndexSchema schema)
			throws ParseException {
		SolrCore core = SolrCore.getSolrCore();
		return (FunctionQuery) (QParser.getParser(func, "func",
				new LocalSolrQueryRequest(core, new HashMap())).parse());
		// return new FunctionQuery(parseValSource(new StrParser(func),
		// schema));
	}

}
